Comparatif des différents types de parallèlisme sur un gros Vision Transformer CoAtNet.

Le but de ce notebook est d'optimiser un code d'apprentissage d'un modèle CoAtNet-7 sur Imagenet pour Jean Zay en implémentant :
Les cellules dans ce notebook ne sont pas prévues pour être modifiées, sauf rares exceptions indiquées dans les commentaires. Les TP se feront en modifiant le code dlojz.py.
Les directives de modification seront marquées par l'étiquette TODO : dans le notebook suivant.
Les solutions sont présentes dans le répertoire solutions/.
Notebook rédigé par l'équipe assistance IA de l'IDRIS, juin 2023
Un module PyTorch doit avoir été chargé pour le bon fonctionnement de ce Notebook. Nécessairement, le module pytorch-gpu/py3/1.11.0 :
!module list
Currently Loaded Modulefiles: 1) cuda/11.2 5) openmpi/4.1.1-cuda 9) sparsehash/2.0.3 2) nccl/2.9.6-1-cuda 6) intel-mkl/2020.4 10) libjpeg-turbo/2.1.3 3) cudnn/8.1.1.33-cuda 7) magma/2.5.4-cuda 11) pytorch-gpu/py3/1.11.0 4) gcc/8.5.0(8.3.1:8.4.1) 8) sox/14.4.2 >
Les fonctions python de gestion de queue SLURM dévelopées par l'IDRIS et les fonctions dédiées à la formation DLO-JZ sont à importer.
Le module d'environnement pour les jobs et la taille des images sont fixés pour ce notebook.
TODO : choisir un pseudonyme (maximum 5 caractères) pour vous différencier dans la queue SLURM et dans les outils collaboratifs pendant la formation.
from idr_pytools import display_slurm_queue, gpu_jobs_submitter, search_log
from dlojz_tools import controle_technique, compare, GPU_underthehood, plot_accuracy, lrfind_plot, pipe_memory, turbo_profiler
MODULE = 'pytorch-gpu/py3/1.13.0'
image_size = 224
account = 'for@v100'
name = 'pseudo' ## TODO Pseudonyme à choisir
Cette partie permet d'afficher et de gérer la queue SLURM.
Pour afficher toute la queue utilisateur :
display_slurm_queue()
Done!
Remarque: Cette fonction utilisée plusieurs fois dans ce notebook permet d'afficher la queue de manière dynamique, rafraichie toutes les 5 secondes. Cependant elle ne s'arrête que lorsque la queue est vide. Si vous désirez reprendre la main sur le notebook, il vous suffira d'arrêter manuellement la cellule avec le bouton stop. Cela a bien sûr aucun impact sur le scheduler SLURM. Les jobs ne seront pas arrêtés.
Si vous voulez arrêter des jobs dans la queue :
#!scancel -u $USER
Cette partie debug permet d'afficher les fichiers de sortie et les fichiers d'erreur du job.
Il est nécessaire dans la cellule suivante (en décommentant) d'indiquer le jobid correspondant sous le format suivant.
*Remarque* : dans ce notebook, lorsque vous soumettrez un job, vous recevrez en retour le numéro du job dans le format suivant : jobid = ['123456']. La cellule ci-dessous peut ainsi être facilement actualisée.
jobid = ['208820']
Fichier de sortie :
%cat {search_log(contains=jobid[0])[0]}
/bin/bash: -c: line 0: syntax error near unexpected token `('
/bin/bash: -c: line 0: `cat {search_log(contains=jobid[0])[0]}'
Fichier d'erreur :
%cat {search_log(contains=jobid[0], with_err=True)['stderr'][0]}
/bin/bash: -c: line 0: syntax error near unexpected token `('
/bin/bash: -c: line 0: `cat {search_log(contains=jobid[0], with_err=True)['stderr'][0]}'
Pour le debug ou pour comparer son code avec les solutions mises à disposition, la fonction suivante permet d'afficher une page html contenant un différentiel de fichiers texte.
s1 = "dlojz.py"
s2 = "./solutions/dlojz4_1.py"
compare(s1, s2)
Voir le résultat du différentiel de fichiers sur la page suivante (attention au spoil !) :
TODO : copier-coller la solution solutions/dlojz4_0.py dans dlojz.py afin d'ajouter dans le code les 2 éléments suivants nécessaires pour la suite des TP :
À noter : Pendant tout le TP, nous utiliserons une taille d'image de 352 x 352, qui correspond à la taille classique utilisée pour ce modèle.
Pour visualiser ces changements, veuillez utiliser le différentiel de fichiers suivant.
s1 = "dlojz.py"
s2 = "./solutions/dlojz4_0.py"
compare(s1, s2)
# copier/coller la solution si nécessaire
!cp solutions/dlojz4_0.py dlojz.py
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
n_gpu = 1
batch_size = 32
command = f'CoAtNet/coatnet.py'
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task Submitted batch job 248347 jobid = ['248347']
Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.
Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.
#jobid = ['1790096']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
248347 gpu_p13 pseudo cfor132 R 1:10 1 r6i3n0
Done!
%cat {search_log(name, contains=jobid[0])[0]}
CoAtNet 0: output shape = torch.Size([1, 1000]), N of Parameters = 20918064 CoAtNet 1: output shape = torch.Size([1, 1000]), N of Parameters = 39890496 CoAtNet 2: output shape = torch.Size([1, 1000]), N of Parameters = 64652672 CoAtNet 3: output shape = torch.Size([1, 1000]), N of Parameters = 131079872 CoAtNet 4: output shape = torch.Size([1, 1000]), N of Parameters = 228407456 CoAtNet 5: output shape = torch.Size([1, 1000]), N of Parameters = 610269344 CoAtNet 6: output shape = torch.Size([1, 1000]), N of Parameters = 1453918848 CoAtNet 7: output shape = torch.Size([1, 1000]), N of Parameters = 2362119808 Mon Jun 26 01:22:31 CEST 2023
TODO : dans le script dlojz.py :
from CoAtNet.coatnet import coatnet_6
Remplacer :
model = models.resnet50() par model = coatnet_6((args.image_size,args.image_size))
et
archi_model = 'Resnet-50' par archi_model = 'CoAtNet-6'
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
n_gpu = 4
batch_size = 4
command = f'dlojz.py -b {batch_size} --image-size {image_size} --test'
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 4 GPUs distributed on 1 nodes with 4 tasks / 4 gpus per node and 10 cpus per task Submitted batch job 248359 jobid = ['248359']
Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.
Puis, rebasculler la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.
#jobid = ['1790206']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
248359 gpu_p13 pseudo cfor132 R 2:45 1 r6i7n6
Done!
controle_technique(jobid)
Train throughput: 37.26 images/second GPU throughput: 37.28 images/second epoch time: 34383.10 seconds training time estimation for 90 epochs (with validations): 863.63 hours ----------- training step time average (fwd/bkwd on GPU): 0.429143 sec (23.9%/75.6%) +/- 0.029793 loading step time average (CPU to GPU): 0.000254 sec +/- 0.000031 ----------- ELIGIBLE to run 0 epochs
Afin de mesurer l'impact de la taille de batch sur l'occupation mémoire et sur le throughput, la cellule suivante permet de soumettre plusieurs jobs avec des tailles de batch croissantes. Dans les cas où la mémoire est saturée et dépasse la capacité du GPU, le système renverra une erreur CUDA Out of Memory.
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
n_gpu = 1
batch_size = [2, 4, 6, 8]
command = [f'dlojz.py -b {b} --image-size {image_size} --test'
for b in batch_size]
jobids = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobids = {jobids}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task Submitted batch job 248362 Submitted batch job 248363 Submitted batch job 248364 Submitted batch job 248365 jobids = ['248362', '248363', '248364', '248365']
Copier-coller la sortie jobids = ['xxxxx', ...] dans la cellule suivante.
Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.
#jobids = ['1790142', '1790143', '1790144', '1790146']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
248364 gpu_p13 pseudo cfor132 R 2:01 1 r6i3n3
Done!
GPU_underthehood(jobids)
Batch size per GPU: 2 Max GPU Memory Allocated: 27.27 GB, Troughput: 6.460 images/second Batch size per GPU: 4 Max GPU Memory Allocated: 28.08 GB, Troughput: 11.127 images/second Batch size per GPU: 6 Max GPU Memory Allocated: 29.83 GB, Troughput: 14.797 images/second Batch size per GPU: 8 CUDA out of memory Memory occupancy by Model part : 26.837 +/- 0.376 GB

Ce TP consiste à implémenter le Pipelined Parallelism de PyTorch et de comparer cette solution avec les autres solutions.
La principale contrainte induite est de structurer le modèle comme suit, avec des torch.nn.Sequential pour chaque section et pour le modèle entier :

Le Pipeline Parallelism de PyTorch est de type standard GPipe.

À noter : Le code modifié permettra de faire de l'Hybrid Parallelism (DP + PP).
Chaque instance créée par Data Parallelism sera associée à une task Slurm, et chacune de ces instances pourra elle-même sollliciter plusieurs GPU pour tourner en mode Pipelined Parallelism.
Dans notre cas, nous testerons le code seulement en mode Pipelined Parallelism, sur 1 task associée à 4 GPU.
TODO : dans le script dlojz.py:
from torch.distributed.pipeline.sync import Pipe
import tempfile
from torch.distributed import rpc
--chunks (pour le nombre de micro batches) avant le parser les arguments.parser.add_argument('--chunks', default=1, type=int, help='number of chunks for Pipelined Parallelism')
args = parser.parse_args()
# Initialize RPC Framework, Pipe depends on it
tmpfile = tempfile.NamedTemporaryFile()
rpc.init_rpc(
name=f'worker{idr_torch.rank}',
rank=0,
world_size=1,
rpc_backend_options=rpc.TensorPipeRpcBackendOptions(
init_method="file://{}".format(tmpfile.name),
# Specifying _transports and _channels is a workaround and we no longer
# will have to specify _transports and _channels for PyTorch
# versions >= 1.8.1 (Not True for Jean Zay)
# With Jean Zay, _transports must be equal to ["shm", "uv"] and not ["ibv", "uv"]
_transports=["shm", "uv"],
_channels=["cuda_ipc", "cuda_basic"],
)
)
# define model
model = coatnet_6((args.image_size,args.image_size))
# How many sections
nb_part = torch.cuda.device_count()//int(os.environ['SLURM_NTASKS_PER_NODE'])
# device number where the first part of the model will run
first_part = idr_torch.local_rank*nb_part
# list of devices involved for pipelined Parallelism
gpus = [g for g in range(first_part, first_part+nb_part)]
class LambdaModule(torch.nn.Module):
def __init__(self, lambd):
super().__init__()
assert isinstance(lambd, type(lambda x: x))
self.lambd = lambd
def forward(self, x):
return self.lambd(x)
lambda_fc = LambdaModule(lambda x: x.view(-1, 2048))
section0 = torch.nn.Sequential(*model.s0, *model.s1, *model.s2, *model.pres3).to(gpus[0])
section1 = torch.nn.Sequential(*model.s3[:15]).to(gpus[1])
section2 = torch.nn.Sequential(*model.s3[15:30]).to(gpus[2])
section3 = torch.nn.Sequential(*model.s3[30:], *model.s4, model.pool, lambda_fc, model.fc).to(gpus[3])
pipe_model = torch.nn.Sequential(*section0, *section1, *section2, *section3)
# Pipe the model, chunks=n means that the batch (size according to batch size) will be shared to n micro batches (size = batch_size/chunks)
model = Pipe(pipe_model, chunks=args.chunks, checkpoint="never")
archi_model = 'CoAtNet-6'
DistributedDataParallel pour prendre en compte le fait qu'il y a plusieurs GPU associés à une seule task pour le Pipelined Parallelism, en indiquant simplement :model = DistributedDataParallel(model)
## Initialisation
if idr_torch.rank == 0: accuracies = []
val_loss = torch.Tensor([0.]).to(gpus[-1]) # send to GPU
val_accuracy = torch.Tensor([0.]).to(gpus[-1]) # send to GPU
# distribution of images and labels to all GPUs
images = images.to(gpus[0], non_blocking=args.non_blocking)
labels = labels.to(gpus[-1], non_blocking=args.non_blocking)
et
# distribution of images and labels to all GPUs
val_images = val_images.to(gpus[0], non_blocking=args.non_blocking)
val_labels = val_labels.to(gpus[-1], non_blocking=args.non_blocking)
Rref, il faudra utiliser la méthode .local_value() pour le transformer en tenseur pour le calcul de la loss, dans les boucles de training et de validation.# Runs the forward pass with autocasting.
with autocast():
outputs = model(images).local_value()
loss = criterion(outputs, labels)
et
# Runs the forward pass with no grade mode.
with torch.no_grad():
with autocast():
val_outputs = model(val_images).local_value()
loss = criterion(val_outputs, val_labels)
else: #
print(f'MaxMemory for GPU:{idr_torch.rank} {torch.cuda.max_memory_allocated()} Bytes') #
#***************************************************************************************************************
for g in gpus: print(f'MaxMemory for GPU:{g} {torch.cuda.max_memory_allocated(device=g)} Bytes')
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
n_gpu = 4
batch_size = 32
command = f'dlojz.py -b {batch_size} --image-size {image_size} --chunks 4 --test'
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name, n_gpu_per_task=4,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 4 GPUs distributed on 1 nodes with 1 tasks / 4 gpus per node and 40 cpus per task Submitted batch job 248369 jobid = ['248369']
Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.
Puis, rebasculler la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.
#jobid = ['1790229']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
248369 gpu_p13 pseudo cfor132 R 4:44 1 r6i7n6
Done!
controle_technique(jobid)
Train throughput: 13.66 images/second GPU throughput: 13.66 images/second epoch time: 93819.94 seconds training time estimation for 90 epochs (with validations): 2375.62 hours ----------- training step time average (fwd/bkwd on GPU): 2.343055 sec (33.5%/68.4%) +/- 0.060034 loading step time average (CPU to GPU): 0.000276 sec +/- 0.000030 ----------- ELIGIBLE to run 1 epochs
pipe_memory(jobid)
Afin de mesurer l'impact de la taille de batch sur l'occupation mémoire et sur le throughput, la cellule suivante permet de soumettre plusieurs jobs avec des tailles de batch croissantes. Dans les cas où la mémoire est saturée et dépasse la capacité du GPU, le système renverra une erreur CUDA Out of Memory.
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
n_gpu = 4
chunks = [(16,2), (32,4), (48,6), (56,7), (64,8)]
command = [f'dlojz.py -b {c[0]} --image-size {image_size} --chunks {c[1]} --test'
for c in chunks]
jobids = gpu_jobs_submitter(command, n_gpu, MODULE, name=name, n_gpu_per_task=4,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobids = {jobids}')
batch job 0: 4 GPUs distributed on 1 nodes with 1 tasks / 4 gpus per node and 40 cpus per task Submitted batch job 248393 Submitted batch job 248394 Submitted batch job 248396 Submitted batch job 248397 Submitted batch job 248398 jobids = ['248393', '248394', '248396', '248397', '248398']
Copier-coller la sortie jobids = ['xxxxx', ...] dans la cellule suivante.
Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.
#jobids = ['1790423', '1790425', '1790428', '1790429', '1790430']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
248397 gpu_p13 pseudo cfor132 R 5:43 1 r7i2n2
Done!
GPU_underthehood(jobids, calcul_memo=True)
Batch size per GPU: 16 Max GPU Memory Allocated: 8.38 GB, Troughput: 10.052 images/second Batch size per GPU: 32 Max GPU Memory Allocated: 15.74 GB, Troughput: 13.239 images/second Batch size per GPU: 48 Max GPU Memory Allocated: 23.21 GB, Troughput: 14.667 images/second Batch size per GPU: 56 Max GPU Memory Allocated: 26.94 GB, Troughput: 15.250 images/second Batch size per GPU: 64 CUDA out of memory Memory occupancy by Model part : 6.511 +/- 5.498 GB
controle_technique([jobids[-2]])
Train throughput: 15.25 images/second GPU throughput: 15.25 images/second epoch time: 84018.02 seconds training time estimation for 90 epochs (with validations): 2128.01 hours ----------- training step time average (fwd/bkwd on GPU): 3.672140 sec (32.7%/66.7%) +/- 0.038428 loading step time average (CPU to GPU): 0.000297 sec +/- 0.000050 ----------- ELIGIBLE to run 1 epochs

# copier/coller la solution si nécessaire
!cp solutions/dlojz4_1.py dlojz.py
Ce TP consiste à implémenter Deepspeed pour intégrer l'optimisation ZeRO pour le Data Parallelism.
TODO : dans le script dlojz.py :
import deepspeed
# Include DeepSpeed configuration arguments
parser = deepspeed.add_config_arguments(parser)
À la place de :
# configure distribution method: define rank and initialise communication backend (NCCL)
dist.init_process_group(backend='nccl', init_method='env://',
world_size=idr_torch.size, rank=idr_torch.rank)
...
model = model.to(gpu)
...
model = DistributedDataParallel(model, device_ids=[idr_torch.local_rank])
...
mettre :
# Deepspeed initialization - force port number if several job run on the same node
deepspeed.init_distributed(distributed_port=os.environ['MASTER_PORT'])
model_engine, optimizer, _, scheduler = deepspeed.initialize(args=args,
model=model,
model_parameters=model.parameters()
)
À noter : Nous garderons, comme indiqué dans la documentation de Deepspeed, la distinction entre le modèle PyTorch model et le modèle encapsulé avec Deepspeed model_engine.
outputs = model_engine(images)
et
val_outputs = model_engine(val_images)
En effet, l'optimisation ZeRO ne supporte pas l'Automatic Mixed Precision. À la place, on appliquera une précision float16 à l'ensemble des paramètres du modèle (cela se fera dans la configuration json).
Pour retrouver les lignes de code à modifier dans le script dlojz.py, vous pouvez utiliser l'outil de différentiel de texte entre la solution dlojz1_1.py et la solution dlojz1_2.py.
float16 afin qu'elles correspondent à la précision du modèle :images = images.half().to(gpu, non_blocking=args.non_blocking, memory_format=torch.channels_last)
et
val_images = val_images.half().to(gpu, non_blocking=args.non_blocking, memory_format=torch.channels_last)
# backward and optimize
loss.backward()
optimizer.step()
par
#runs backpropagation
model_engine.backward(loss)
#weight update
model_engine.step()
# scheduler update
#scheduler.step()
La configuration de Deepspeed se fait par fichier JSON :
%%writefile ds_config.json
{ "train_micro_batch_size_per_gpu": 16,
"gradient _accumulation_steps": 1,
"optimizer": {
"type": "Adam",
"params": {
"lr": 0.001,
"weight_decay": 5e-4
}
},
"scheduler": {
"type": "OneCycle",
"params": {
"cycle_min_lr": 1e-6,
"cycle_max_lr": 1e-3,
"decay_lr_rate": 1e-6
}
},
"fp16": {
"enabled": true,
"loss_scale": 0,
"initial_scale_power": 32,
"loss_scale_window": 1000,
"hysteresis": 2,
"min_loss_scale": 1
},
"zero_optimization": {
"stage": 2
},
"zero_allow_untested_optimizer": true
}
Overwriting ds_config.json
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
n_gpu = 4
batch_size = 16
command = f'dlojz.py -b {batch_size} --image-size {image_size} --test --deepspeed --deepspeed_config ds_config.json'
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 4 GPUs distributed on 1 nodes with 4 tasks / 4 gpus per node and 10 cpus per task Submitted batch job 248510 jobid = ['248510']
Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.
Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.
#jobid = ['248508']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
248511 gpu_p5 pseudo cfor132 R 0:46 1 jean-zay-iam01
Key interrupt
controle_technique(jobid)
Train throughput: 94.35 images/second GPU throughput: 94.93 images/second epoch time: 13579.50 seconds training time estimation for 90 epochs (with validations): 343.32 hours ----------- training step time average (fwd/bkwd on GPU): 0.674188 sec (26.7%/66.0%) +/- 0.061517 loading step time average (CPU to GPU): 0.004142 sec +/- 0.002663 ----------- ELIGIBLE to run 1 epochs
pipe_memory(jobid)
Afin de mesurer l'impact de la taille de batch sur l'occupation mémoire et sur le throughput, la cellule suivante permet de soumettre plusieurs jobs avec des tailles de batch croissantes. Dans les cas où la mémoire est saturée et dépasse la capacité du GPU, le système renverra une erreur CUDA Out of Memory.
import json
batch_size = [2, 4, 8, 16, 24, 32]
for b in batch_size:
with open("ds_config.json", "r") as jsonFile:
data = json.load(jsonFile)
data["train_micro_batch_size_per_gpu"] = b
with open(f"ds_config{b}.json", "w") as jsonFile:
json.dump(data, jsonFile)
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
n_gpu = 4
command = [f'dlojz.py -b {b} --image-size {image_size} --test --deepspeed --deepspeed_config ds_config{b}.json'
for b in batch_size]
jobids = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobids = {jobids}')
batch job 0: 4 GPUs distributed on 1 nodes with 4 tasks / 4 gpus per node and 10 cpus per task Submitted batch job 248497 Submitted batch job 248498 Submitted batch job 248499 Submitted batch job 248500 Submitted batch job 248502 Submitted batch job 248503 jobids = ['248497', '248498', '248499', '248500', '248502', '248503']
Copier-coller la sortie jobids = ['xxxxx', ...] dans la cellule suivante.
Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.
#jobids = ['169112', '169113', '169114', '169115', '169116', '169117']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
248502 gpu_p13 pseudo cfor132 R 2:51 1 r8i2n6
Done!
GPU_underthehood(jobids)
Batch size per GPU: 8 Max GPU Memory Allocated: 11.76 GB, Troughput: 26.472 images/second Batch size per GPU: 16 Max GPU Memory Allocated: 11.76 GB, Troughput: 46.642 images/second Batch size per GPU: 32 Max GPU Memory Allocated: 13.53 GB, Troughput: 72.730 images/second Batch size per GPU: 64 Max GPU Memory Allocated: 19.12 GB, Troughput: 93.009 images/second Batch size per GPU: 96 Max GPU Memory Allocated: 24.85 GB, Troughput: 103.909 images/second Batch size per GPU: 128 CUDA out of memory Memory occupancy by Model part : 9.899 +/- 1.562 GB
controle_technique([jobids[-2]])
Train throughput: 103.39 images/second GPU throughput: 103.91 images/second epoch time: 12391.75 seconds training time estimation for 90 epochs (with validations): 313.71 hours ----------- training step time average (fwd/bkwd on GPU): 0.923888 sec (29.2%/64.8%) +/- 0.069357 loading step time average (CPU to GPU): 0.004611 sec +/- 0.002797 ----------- ELIGIBLE to run 1 epochs

Ce TP consite à implémenter le Pipeline Parallelism de Deepspeed que l'on pourra ensuite utiliser en mode hybride avec le Data Parallelism + ZeRO.
La version du Pipelined Parallelism de Deepspeed est optimisé pour économiser l'empreinte mémoire.

À noter : Avec Deepspeed, le Pipelined Parallelism comme le Data Parallism fonctionne toujours en multi-task, ainsi une task est associée à chaque device.
L'implémentation du Pipeline Parallelism amenant trop de changements par rapport au code manipulé durant le TP, nous vous suggérons de copier-coller la solution solutions/dlojz4_4.py sur dlojz.py.
TODO :
solutions/dlojz4_4.py sur dlojz.py.# Define Pipeline Module
deepspeed.init_distributed(distributed_port=os.environ['MASTER_PORT'])
model = PipelineModule(layers = [
*model.s0, *model.s1, *model.s2, *model.pres3, *model.s3, *model.s4,
model.pool, lambda x: x.view(-1, 2048), model.fc],
num_stages = args.nb_pipeline_stages,
loss_fn=criterion,
partition_method = 'parameters' if args.partition_param else 'uniform')
# Deepspeed initialization - force port number if several job run on the same node
model_engine, optimizer, _, scheduler = deepspeed.initialize(args=args,
model=model,
model_parameters=model.parameters(),
training_data=train_dataset)
...
loss = model_engine.train_batch()
....
val_loss = model_engine.eval_batch(val_iter)
À noter : la configuration du Pipeline Parallelism se fait avec :
train_micro_batch_size_per_gpu correspondant à la taille du micro batch,gradient_accumulation_steps correspondant au nombre de tronçons du pipeline.La taille du mini batch pour chaque itération d'apprentissage correspond donc à train_micro_batch_size_per_gpu x gradient_accumulation_steps.
%%writefile ds_config.json
{ "train_micro_batch_size_per_gpu": 24,
"gradient_accumulation_steps": 8,
"optimizer": {
"type": "AdamW",
"params": {
"lr": 0.001,
"weight_decay": 5e-4
}
},
"scheduler": {
"type": "OneCycle",
"params": {
"cycle_min_lr": 1e-6,
"cycle_max_lr": 1e-3,
"decay_lr_rate": 1e-6
}
},
"fp16": {
"enabled": true,
"loss_scale": 0,
"initial_scale_power": 32,
"loss_scale_window": 1000,
"hysteresis": 2,
"min_loss_scale": 1
},
"zero_allow_untested_optimizer": true
}
Overwriting ds_config.json
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
n_gpu = 4
command = f'dlojz.py --image-size {image_size} -p 4 --test --deepspeed --deepspeed_config ds_config.json'
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 4 GPUs distributed on 1 nodes with 4 tasks / 4 gpus per node and 10 cpus per task Submitted batch job 248512 jobid = ['248512']
Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.
Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.
#jobid = ['230538']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
248512 gpu_p13 pseudo cfor132 R 2:19 1 r6i7n6
Done!
controle_technique(jobid)
Train throughput: 77.30 images/second GPU throughput: 77.30 images/second epoch time: 16573.70 seconds training time estimation for 90 epochs (with validations): 420.70 hours ----------- training step time average (fwd/bkwd on GPU): 2.483694 sec (nan%/nan%) +/- 0.064397 loading step time average (CPU to GPU): 0.000002 sec +/- 0.000000 ----------- ELIGIBLE to run 1 epochs
pipe_memory(jobid)
Afin de mesurer l'impact de la taille de batch sur l'occupation mémoire et sur le throughput, la cellule suivante permet de soumettre plusieurs jobs avec des tailles de batch croissantes. Dans les cas où la mémoire est saturée et dépasse la capacité du GPU, le système renverra une erreur CUDA Out of Memory.
import json
chunks_numbers = [2, 4, 8, 16, 32, 40]
for c in chunks_numbers:
with open("ds_config.json", "r") as jsonFile:
data = json.load(jsonFile)
data["gradient_accumulation_steps"] = c
with open(f"ds_config{c}.json", "w") as jsonFile:
json.dump(data, jsonFile)
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
n_gpu = 4
command = [f'dlojz.py --image-size {image_size} -p 4 --test --deepspeed --deepspeed_config ds_config{c}.json'
for c in chunks_numbers]
jobids = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, time_max='00:20:00', constraint='v100-32g')
print(f'jobids = {jobids}')
batch job 0: 4 GPUs distributed on 1 nodes with 4 tasks / 4 gpus per node and 10 cpus per task Submitted batch job 248513 Submitted batch job 248514 Submitted batch job 248515 Submitted batch job 248516 Submitted batch job 248517 Submitted batch job 248518 jobids = ['248513', '248514', '248515', '248516', '248517', '248518']
Copier-coller la sortie jobids = ['xxxxx', ...] dans la cellule suivante.
Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.
#jobids = ['239664', '239666', '239667', '239668', '239674', '239676']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
248518 gpu_p13 pseudo cfor132 R 7:05 1 r8i2n6
Done!
GPU_underthehood(jobids, calcul_memo=True)
/gpfsdswork/projects/idris/for/cfor132/dlo-jz/dlojz_tools.py:250: FutureWarning: elementwise comparison failed; returning scalar instead, but in the future will perform elementwise comparison
Batch size per GPU: 48 Max GPU Memory Allocated: 11.67 GB, Troughput: 48.153 images/second Batch size per GPU: 96 Max GPU Memory Allocated: 22.68 GB, Troughput: 62.772 images/second Batch size per GPU: 192 Max GPU Memory Allocated: 22.79 GB, Troughput: 76.598 images/second Batch size per GPU: 384 Max GPU Memory Allocated: 22.79 GB, Troughput: 83.476 images/second Batch size per GPU: 768 Max GPU Memory Allocated: 22.79 GB, Troughput: 89.876 images/second Batch size per GPU: 960 Max GPU Memory Allocated: 22.79 GB, Troughput: 91.305 images/second Memory occupancy by Model part : 18.320 +/- 8.831 GB
controle_technique([jobids[-1]])
Train throughput: 91.31 images/second GPU throughput: 91.31 images/second epoch time: 14036.44 seconds training time estimation for 90 epochs (with validations): 356.08 hours ----------- training step time average (fwd/bkwd on GPU): 10.514183 sec (nan%/nan%) +/- 0.250693 loading step time average (CPU to GPU): 0.000003 sec +/- 0.000000 ----------- ELIGIBLE to run 1 epochs
TODO : Trouver la meilleure architecture et configuration en terme de Throughput.
L'argument -p correspond au nombre de stages du pipeline. Sachant que l'on utilise 4 GPU, un stage de 4 correspond à un pipeline parallelism total sur 4 GPU, un stage de 2 correspond à un hybrid parallelism 2x2, un stage de 1 à un Data Parallelism complet.
Choisir un optimiseur accéléré comme : Adam, AdamW, Lamb, OnebitAdam, OnebitLamb, ou ZeroOneAdam.
Configuration JSON :
À noter :
OnebitAdam, OnebitLamb, ou ZeroOneAdam ne marche pas avec ZeRO. Si vous utilisez un de ceux-ci, il faudra mettre le paramètre freeze_step comme ceci pour pouvoir mesurer son accélération dans notre test :"optimizer": {
"type": "OnebitAdam",
"params": {
"lr": 0.001,
"weight_decay": 5e-4,
"freeze_step": 5
}
},
%%writefile ds_config.json
{ "train_micro_batch_size_per_gpu": 16,
"gradient_accumulation_steps": 24,
"optimizer": {
"type": "AdamW",
"params": {
"lr": 0.001,
"weight_decay": 5e-2
}
},
"scheduler": {
"type": "OneCycle",
"params": {
"cycle_min_lr": 1e-6,
"cycle_max_lr": 1e-3,
"decay_lr_rate": 1e-6
}
},
"fp16": {
"enabled": true,
"loss_scale": 0,
"initial_scale_power": 32,
"loss_scale_window": 1000,
"hysteresis": 2,
"min_loss_scale": 1
},
"zero_optimization": {
"stage": 1
},
"zero_allow_untested_optimizer": true
}
Overwriting ds_config.json
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
n_gpu = 4
command = f'dlojz.py --image-size {image_size} -p 2 --test --deepspeed --deepspeed_config ds_config.json'
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, time_max='00:20:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 4 GPUs distributed on 1 nodes with 4 tasks / 4 gpus per node and 10 cpus per task Submitted batch job 248523 jobid = ['248523']
Copier-coller vos sorties jobid = ['xxxxx'] dans la cellule suivante.
#jobid = ['202876']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
248523 gpu_p13 pseudo cfor132 R 5:25 1 r6i7n6
Done!
controle_technique(jobid)
Train throughput: 98.05 images/second GPU throughput: 98.05 images/second epoch time: 13072.98 seconds training time estimation for 90 epochs (with validations): 331.45 hours ----------- training step time average (fwd/bkwd on GPU): 7.832820 sec (nan%/nan%) +/- 0.201737 loading step time average (CPU to GPU): 0.000002 sec +/- 0.000000 ----------- ELIGIBLE to run 1 epochs
pipe_memory(jobid)
%cat {search_log(contains=jobid[0])[0]}
%cat {search_log(contains=jobid[0], with_err=True)['stderr'][0]}
